﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Globalization;
using System.Reflection;
using Microsoft.Internal;
using Microsoft.Internal.Collections;

namespace System.ComponentModel.Composition.ReflectionModel
{
    internal class ImportingMember : ImportingItem
    {
        private readonly ReflectionWritableMember _member;

        public ImportingMember(ContractBasedImportDefinition definition, ReflectionWritableMember member)
            : base(definition, member)
        {
            Assumes.NotNull(definition, member);

            _member = member;
        }

        public void SetExportedObject(object instance, object value)
        {
            if (RequiresCollectionNormalization(value))
            {
                this.SetCollectionMemberValue(instance, (IEnumerable)value);
            }
            else
            {
                this.SetSingleMemberValue(instance, value);
            }
        }

        private void SetCollectionMemberValue(object instance, IEnumerable values)
        {
            Assumes.NotNull(values);

            MethodInfo add, clear;
            if (!IsCollectionType(out add, out clear))
            {
                throw new ComposablePartException(
                    CompositionErrorId.ReflectionModel_ImportCollectionNotWritable,
                    String.Format(CultureInfo.CurrentCulture, 
                        Strings.ReflectionModel_ImportCollectionNotWritable,
                        this._member.DisplayName,
                        values.GetType().FullName),
                    Definition.ToElement());
            }

            // BUG: Dev10 406227 We need to wrap this in InvokeWrappingInCatch
            object collection = this._member.GetValue(instance);
            if (collection == null)
            {
                throw new ComposablePartException(
                    CompositionErrorId.ReflectionModel_ImportCollectionNull,
                    String.Format(CultureInfo.CurrentCulture, 
                        Strings.ReflectionModel_ImportCollectionNull,
                        this._member.DisplayName,
                        values.GetType().FullName),
                    Definition.ToElement());
            }

            PopulateCollection(collection, values, add, clear);
        }

        private static void PopulateCollection(object collection, IEnumerable values, MethodInfo add, MethodInfo clear)
        {
            // BUG: Dev10 406227 will make this a little more consistent
            // Need to make sure that any reflection calls (ie to Add 
            // or Clear) are wrapped, and reported appropriately.

            clear.Invoke(collection, (object[])null);

            foreach (object value in values)
            {
                add.Invoke(collection, new object[] { value });
            }
        }

        private void SetSingleMemberValue(object instance, object value)
        {
            EnsureWritable();
            
            Exception exception = ReflectionServices.InvokeWrappingInCatch(() => this._member.SetValue(instance, value));
            if (exception != null)
            {   // Member threw an exception. Avoid letting this 
                // leak out as a 'raw' unhandled exception, instead,
                // we'll add some context and rethrow.

                throw new ComposablePartException(
                    CompositionErrorId.ReflectionModel_ImportThrewException,
                    String.Format(CultureInfo.CurrentCulture, 
                        Strings.ReflectionModel_ImportThrewException,
                        this._member.DisplayName),
                    Definition.ToElement(),
                    exception);
            }
        }

        private bool RequiresCollectionNormalization(object value)
        {
            if (this.Definition.Cardinality != ImportCardinality.ZeroOrMore)
            {   // If we're not looking at a collection import, then don't 
                // 'normalize' the collection.

                return false;
            }

            if (this._member.CanWrite && this.ImportType.Type.IsAssignableFrom(value.GetType()))
            {   // If we can simply replace the entire value of the property/field, then 
                // we don't need to 'normalize' the collection.

                return false;
            }

            return true;
        }

        private bool IsCollectionType(out MethodInfo add, out MethodInfo clear)
        {
            // NOTE: Make sure you base the 'collection' type off the actual
            // member type, and not the underlying object returned by the 
            // member. This prevents a non-breaking change (ie returning a 
            // different type of object) from breaking composition.
            Type collectionType = this.ImportType.Type;

            if (!TryGetCollectionMethods(collectionType, out add, out clear))
            {
                return false;
            }

            return CollectionServices.IsCollectionType(collectionType);
        }

        private static bool TryGetCollectionMethods(Type collectionType, out MethodInfo add, out MethodInfo clear)
        {
            // BUG: Dev10 406227 will make this a better. Check out 
            // WeakCollection.cs which has an approach for finding
            // the Add/Clear methods.
            add = collectionType.GetMethod("Add");
            clear = collectionType.GetMethod("Clear");

            if (add == null || clear == null)
            {
                // We need to walk the interfaces because members from interfaces
                // are not considered as inherited (even if collectionType is an 
                // interface).
                foreach (Type type in collectionType.GetInterfaces())
                {
                    add = add ?? type.GetMethod("Add");
                    clear = clear ?? type.GetMethod("Clear");
                }
            }

            return add != null && clear != null;
        }

        private void EnsureWritable()
        {
            if (!this._member.CanWrite)
            {   // Property does not have a setter, or 
                // field is marked as read-only.

                throw new ComposablePartException(
                    CompositionErrorId.ReflectionModel_ImportNotWritable,
                    String.Format(CultureInfo.CurrentCulture, 
                        Strings.ReflectionModel_ImportNotWritable,
                        this._member.DisplayName),
                        Definition.ToElement());
            }
        }
    }
}
